import {
  Injectable,
  NotFoundException,
  BadRequestException,
  UnauthorizedException,
  ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like, MoreThanOrEqual, In } from 'typeorm';
import { User } from './entities/user.entity';
import { UpdateUserDto } from './dto/update-user.dto';
import { UpdateUserStatusDto } from './dto/update-user-status.dto';
import * as argon2 from 'argon2';
import { RedisService } from '../auth/redis/redis.service';
import { UserRole } from '../common/enums';
import { UserResponseDto, UserAdminResponseDto } from './dto/user-response.dto';
import { Device } from '../devices/entities/device.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    @InjectRepository(Device)
    private devicesRepository: Repository<Device>,
    private redisService: RedisService,
  ) {}

  async findAll(options: {
    page: number;
    limit: number;
    search?: string;
    role?: UserRole;
    isActive?: boolean;
    createdAfter?: string;
    deviceSerialNumber?: string;
  }) {
    const { page, limit, search, role, isActive, createdAfter, deviceSerialNumber } = options;

    // Calculate skip for pagination
    const skip = (page - 1) * limit;

    // Build where clause for search
    let whereOptions: any = {};

    // Add search condition
    if (search) {
      whereOptions = [{ username: Like(`%${search}%`) }, { displayName: Like(`%${search}%`) }];
    }

    // Add role filter
    if (role) {
      if (Array.isArray(whereOptions)) {
        whereOptions = whereOptions.map((condition) => ({ ...condition, role }));
      } else {
        whereOptions.role = role;
      }
    }

    // Add isActive filter
    if (isActive !== undefined) {
      if (Array.isArray(whereOptions)) {
        whereOptions = whereOptions.map((condition) => ({ ...condition, isActive }));
      } else {
        whereOptions.isActive = isActive;
      }
    }

    // Add createdAfter filter
    if (createdAfter) {
      const createdAfterDate = new Date(createdAfter);
      if (!isNaN(createdAfterDate.getTime())) {
        if (Array.isArray(whereOptions)) {
          whereOptions = whereOptions.map((condition) => ({
            ...condition,
            createdAt: MoreThanOrEqual(createdAfterDate),
          }));
        } else {
          whereOptions.createdAt = MoreThanOrEqual(createdAfterDate);
        }
      }
    }

    // If filtering by device serial number, first get the user IDs from devices
    let userIds: string[] | undefined;
    if (deviceSerialNumber) {
      const devices = await this.devicesRepository.find({
        where: { serialNumber: Like(`%${deviceSerialNumber}%`) },
        select: ['userId'],
      });
      userIds = devices.map((device) => device.userId);

      // If no users found with matching devices, return empty result
      if (userIds.length === 0) {
        return {
          items: [],
          meta: {
            total: 0,
            page,
            limit,
            pages: 0,
          },
        };
      }

      // Add user ID filter to where options
      if (Array.isArray(whereOptions)) {
        whereOptions = whereOptions.map((condition) => ({ ...condition, id: In(userIds) }));
      } else {
        whereOptions.id = In(userIds);
      }
    }

    // Get users with pagination
    const [users, total] = await this.usersRepository.findAndCount({
      where: whereOptions,
      skip,
      take: limit,
      order: { createdAt: 'DESC' },
    });

    // If no users found, return empty result
    if (users.length === 0) {
      return {
        items: [],
        meta: {
          total: 0,
          page,
          limit,
          pages: 0,
        },
      };
    }

    // Get all devices for these users in a single query
    const userDeviceIds = users.map((user) => user.id);
    const devices = await this.devicesRepository.find({
      where: { userId: In(userDeviceIds) },
      order: { lastUsedAt: 'DESC' },
    });

    // Group devices by userId
    const devicesByUserId = devices.reduce((acc, device) => {
      if (!acc[device.userId]) {
        acc[device.userId] = [];
      }
      acc[device.userId].push(device);
      return acc;
    }, {});

    // Map to response DTOs with admin context and devices
    const items = users.map((user) => {
      const userWithDevices = {
        ...user,
        devices: devicesByUserId[user.id] || [],
      };
      return this.mapToResponseDto(userWithDevices, true);
    });

    return {
      items,
      meta: {
        total,
        page,
        limit,
        pages: Math.ceil(total / limit),
      },
    };
  }

  /**
   * Find a user by ID
   * @param id User ID
   * @returns User or null if not found
   */
  async findById(id: string) {
    const user = await this.usersRepository.findOne({
      where: { id },
    });

    if (!user) {
      return null;
    }

    // Get all devices for this user
    const devices = await this.devicesRepository.find({
      where: { userId: id },
    });

    // Create a user object with devices
    return {
      ...user,
      devices,
    };
  }

  /**
   * Find a user by username
   * @param username Username
   * @returns User or null if not found
   */
  async findByUsername(username: string) {
    return this.usersRepository.findOne({ where: { username } });
  }

  /**
   * Create a new user
   * @param userData User data
   * @returns Created user
   */
  async create(userData: {
    username: string;
    passwordHash: string;
    role: any;
    displayName?: string;
    phone?: string;
  }) {
    const user = this.usersRepository.create({
      username: userData.username,
      passwordHash: userData.passwordHash,
      role: userData.role,
      displayName: userData.displayName || userData.username.split('@')[0],
      phone: userData.phone || null,
    });

    return this.usersRepository.save(user);
  }

  /**
   * Create a new user by admin
   * @param createUserDto User data from admin
   * @returns Created user
   */
  async createByAdmin(createUserDto: {
    username: string;
    password: string;
    role: any;
    displayName?: string;
    phone?: string;
  }): Promise<UserResponseDto> {
    // Check if username already exists
    const existingUser = await this.findByUsername(createUserDto.username);
    if (existingUser) {
      throw new Error(`User with username ${createUserDto.username} already exists`);
    }

    // Hash password
    const passwordHash = await argon2.hash(createUserDto.password);

    // Create user
    const user = await this.create({
      username: createUserDto.username,
      passwordHash,
      role: createUserDto.role,
      displayName: createUserDto.displayName,
      phone: createUserDto.phone,
    });

    return this.mapToResponseDto(user);
  }

  async findOne(id: string) {
    const user = await this.usersRepository.findOne({
      where: { id },
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Get all devices for this user
    const devices = await this.devicesRepository.find({
      where: { userId: id },
    });

    // Create a user object with devices
    const userWithDevices = {
      ...user,
      devices,
    };

    return this.mapToResponseDto(userWithDevices, true);
  }

  async update(id: string, updateUserDto: UpdateUserDto, isAdmin: boolean) {
    const user = await this.usersRepository.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Update fields
    if (updateUserDto.displayName !== undefined) {
      user.displayName = updateUserDto.displayName;
    }

    if (updateUserDto.phone !== undefined) {
      user.phone = updateUserDto.phone;
    }

    // Only admin can update active status
    if (isAdmin && updateUserDto.isActive !== undefined) {
      user.isActive = updateUserDto.isActive;
    }

    // Save updated user
    const updatedUser = await this.usersRepository.save(user);

    return this.mapToResponseDto(updatedUser);
  }

  async updateStatus(id: string, updateUserStatusDto: UpdateUserStatusDto) {
    const user = await this.usersRepository.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Update status
    user.isActive = updateUserStatusDto.isActive;

    // If deactivating, store reason
    if (!updateUserStatusDto.isActive && updateUserStatusDto.deactivationReason) {
      user.deactivationReason = updateUserStatusDto.deactivationReason;
    }

    // If activating, clear deactivation reason
    if (updateUserStatusDto.isActive) {
      user.deactivationReason = null;
    }

    // Save updated user
    const updatedUser = await this.usersRepository.save(user);

    // If deactivating, invalidate all sessions
    if (!updateUserStatusDto.isActive) {
      await this.redisService.deleteUserSessions(id);
    }

    return this.mapToResponseDto(updatedUser);
  }

  async changePassword(id: string, oldPassword: string, newPassword: string) {
    const user = await this.usersRepository.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Verify old password
    const isPasswordValid = await argon2.verify(user.passwordHash, oldPassword);
    if (!isPasswordValid) {
      throw new UnauthorizedException('Current password is incorrect');
    }

    // Hash new password
    const newPasswordHash = await argon2.hash(newPassword);

    // Update password
    user.passwordHash = newPasswordHash;
    await this.usersRepository.save(user);

    // Invalidate all sessions (force re-login)
    await this.redisService.deleteUserSessions(id);
  }

  async remove(id: string) {
    const user = await this.usersRepository.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Invalidate all sessions
    await this.redisService.deleteUserSessions(id);

    // Delete user
    await this.usersRepository.remove(user);
  }

  async findByPhone(phone: string): Promise<User | null> {
    return this.usersRepository.findOne({ where: { phone } });
  }

  async resetPassword(id: string, passwordHash: string): Promise<void> {
    const user = await this.usersRepository.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Update password
    user.passwordHash = passwordHash;
    await this.usersRepository.save(user);

    // Invalidate all sessions (force re-login)
    await this.redisService.deleteUserSessions(id);
  }

  async resetPasswordByAdmin(
    id: string,
    newPassword: string,
    forceLogout: boolean = true,
  ): Promise<void> {
    const user = await this.usersRepository.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Check if trying to reset admin password (only allow if current user is super admin)
    if (user.role === UserRole.ADMIN) {
      throw new ForbiddenException('Cannot reset admin password');
    }

    // Hash new password
    const newPasswordHash = await argon2.hash(newPassword);

    // Update password
    user.passwordHash = newPasswordHash;
    await this.usersRepository.save(user);

    // Invalidate all sessions if forceLogout is true
    if (forceLogout) {
      await this.redisService.deleteUserSessions(id);
    }
  }

  private mapToResponseDto(
    user: User,
    isAdmin: boolean = false,
  ): UserResponseDto | UserAdminResponseDto {
    const baseResponse = {
      id: user.id,
      username: user.username,
      role: user.role,
      displayName: user.displayName,
      phone: user.phone,
      isActive: user.isActive,
      lastLoginAt: user.lastLoginAt,
      createdAt: user.createdAt,
    };

    const devices = user.devices || [];

    if (isAdmin) {
      return {
        ...baseResponse,
        devices: devices.map((device) => ({
          id: device.id,
          serialNumber: device.serialNumber,
          status: device.status,
          registeredBy: device.registeredBy,
          registeredAt: device.registeredAt,
          lastUsedAt: device.lastUsedAt,
          userId: device.userId,
          metadata: device.metadata,
        })),
      };
    }

    return {
      ...baseResponse,
      devices: devices.map((device) => ({
        serialNumber: device.serialNumber,
        status: device.status,
        lastUsedAt: device.lastUsedAt,
      })),
    };
  }

  async getMe(user: User) {
    // Get all devices for this user
    const devices = await this.devicesRepository.find({
      where: { userId: user.id },
    });

    // Create a user object with devices
    const userWithDevices = {
      ...user,
      devices,
    };

    return this.mapToResponseDto(userWithDevices, false);
  }
}
